﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Transactions;

namespace ServerUtils.Test
{
    [TestClass]
    public class MRInvokerWithTransactionTest
    {
        private MRInvokerWithTransaction target;
        private int maxRetry = 4;
        private int retryInterval = 100;
        private List<InvokerErrorEventArgs> errors;
        private bool handleException;
        private List<TransactionStatus> transactionStatusList = new List<TransactionStatus>();
        private TransactionalOperationForObject transactionalOperation = new TransactionalOperationForObject { CanCommit = true, Result = new object() };

        [TestInitialize]
        public void Initialize()
        {
            this.target = new MRInvokerWithTransaction(this.retryInterval, this.maxRetry, IsolationLevel.ReadCommitted);
            this.target.Error += this.TargetError;

            this.errors = new List<InvokerErrorEventArgs>();
            this.handleException = true;
        }

        [TestMethod]
        public void GivenConstructorParameters_TestIsolationLevel()
        {
            IsolationLevel expectedIsolationLevel;
            IsolationLevel actualIsolationLevel = IsolationLevel.Unspecified;
            MRInvokerWithTransaction invoker;

            expectedIsolationLevel = IsolationLevel.ReadCommitted;
            invoker = new MRInvokerWithTransaction(this.retryInterval, this.maxRetry, expectedIsolationLevel);
            invoker.Invoke(() =>
            {
                actualIsolationLevel = Transaction.Current.IsolationLevel;
            });

            Assert.AreEqual(expectedIsolationLevel, actualIsolationLevel);

            expectedIsolationLevel = IsolationLevel.Chaos;
            invoker = new MRInvokerWithTransaction(this.retryInterval, this.maxRetry, expectedIsolationLevel);
            invoker.Invoke(() =>
            {
                actualIsolationLevel = Transaction.Current.IsolationLevel;
            });

            Assert.AreEqual(expectedIsolationLevel, actualIsolationLevel);
        }

        #region Invoke Action

        [TestMethod]
        public void InvokeAction_GivenAction_AnActiveAmbientTransactionIsPresent()
        {
            Transaction ambienttransaction = null;
            TransactionStatus? transactionStatus = null;
            this.target.Invoke(() =>
            {
                ambienttransaction = Transaction.Current;
                transactionStatus = ambienttransaction.TransactionInformation.Status;
            });

            Assert.IsNotNull(ambienttransaction);
            Assert.AreEqual(TransactionStatus.Active, transactionStatus.Value);
        }

        [TestMethod]
        public void InvokeAction_GivenAction_TransactionIsCommitted()
        {
            this.target.Invoke(() =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
            });

            Assert.AreEqual(1, this.transactionStatusList.Count);
            Assert.AreEqual(TransactionStatus.Committed, this.transactionStatusList[0]);
        }

        [TestMethod]
        public void InvokeAction_GivenExceptionInAction_TransactionIsAborted()
        {
            try
            {
                this.target.Invoke(() =>
                {
                    Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                    throw new Exception();
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.transactionStatusList.Count);
            for (int i = 0; i < this.maxRetry; i++)
            {
                Assert.AreEqual(TransactionStatus.Aborted, this.transactionStatusList[i]);
            }
        }

        #endregion

        #region Invoke Function

        [TestMethod]
        public void InvokeFunction_GivenFunction_AnActiveAmbientTransactionIsPresent()
        {
            Transaction ambienttransaction = null;
            TransactionStatus? transactionStatus = null;
            this.target.Invoke<object>(() =>
            {
                ambienttransaction = Transaction.Current;
                transactionStatus = ambienttransaction.TransactionInformation.Status;
                return this.transactionalOperation;
            });

            Assert.IsNotNull(ambienttransaction);
            Assert.AreEqual(TransactionStatus.Active, transactionStatus.Value);
        }

        [TestMethod]
        public void InvokeFunction_GivenSucceedFunction_TransactionIsCommitted()
        {
            this.target.Invoke<object>(() =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                return this.transactionalOperation;
            });

            Assert.AreEqual(1, this.transactionStatusList.Count);
            Assert.AreEqual(TransactionStatus.Committed, this.transactionStatusList[0]);
        }

        [TestMethod]
        public void InvokeFunction_GivenSucceedFunction_FunctionResultIsReturned()
        {
            var actual = this.target.Invoke<object>(() =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                return this.transactionalOperation;
            });

            Assert.AreSame(this.transactionalOperation.Result, actual);
        }

        [TestMethod]
        public void InvokeFunction_GivenFailedFunction_TransactionIsAborted()
        {
            this.transactionalOperation.CanCommit = false;
            this.target.Invoke<object>(() =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                return this.transactionalOperation;
            });

            Assert.AreEqual(1, this.transactionStatusList.Count);
            Assert.AreEqual(TransactionStatus.Aborted, this.transactionStatusList[0]);
        }

        [TestMethod]
        public void InvokeFunction_GivenExceptionInFunction_TransactionIsAborted()
        {
            try
            {
                this.target.Invoke<object>(() =>
                {
                    Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                    throw new Exception();
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.transactionStatusList.Count);
            for (int i = 0; i < this.maxRetry; i++)
            {
                Assert.AreEqual(TransactionStatus.Aborted, this.transactionStatusList[i]);
            }
        }

        #endregion

        #region Invoke Async Action

        [TestMethod]
        public async Task InvokeAsyncAction_GivenAction_AnActiveAmbientTransactionIsPresent()
        {
            Transaction ambienttransaction = null;
            TransactionStatus? transactionStatus = null;
            await this.target.InvokeAsync(async () =>
            {
                ambienttransaction = Transaction.Current;
                transactionStatus = ambienttransaction.TransactionInformation.Status;
                await Task.Delay(1);
            });

            Assert.IsNotNull(ambienttransaction);
            Assert.AreEqual(TransactionStatus.Active, transactionStatus.Value);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenAction_TransactionIsCommitted()
        {
            await this.target.InvokeAsync(async () =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                await Task.Delay(1);
            });

            Assert.AreEqual(1, this.transactionStatusList.Count);
            Assert.AreEqual(TransactionStatus.Committed, this.transactionStatusList[0]);
        }

        [TestMethod]
        public async Task InvokeAsyncAction_GivenExceptionInAction_TransactionIsAborted()
        {
            try
            {
                await this.target.InvokeAsync(() =>
                {
                    Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                    throw new Exception();
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.transactionStatusList.Count);
            for (int i = 0; i < this.maxRetry; i++)
            {
                Assert.AreEqual(TransactionStatus.Aborted, this.transactionStatusList[i]);
            }
        }

        #endregion

        #region Invoke Async Function

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenFunction_AnActiveAmbientTransactionIsPresent()
        {
            Transaction ambienttransaction = null;
            TransactionStatus? transactionStatus = null;
            await this.target.InvokeAsync<object>(async () =>
            {
                ambienttransaction = Transaction.Current;
                transactionStatus = ambienttransaction.TransactionInformation.Status;
                return await Task<TransactionalOperationForObject>.Run(() =>
                {
                    Thread.Sleep(100);
                    return this.transactionalOperation;
                });
            });

            Assert.IsNotNull(ambienttransaction);
            Assert.AreEqual(TransactionStatus.Active, transactionStatus.Value);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenSucceedFunction_TransactionIsCommitted()
        {
            await this.target.InvokeAsync<object>(async () =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                return await Task<TransactionalOperationForObject>.Run(() => { return this.transactionalOperation; });
            });

            Assert.AreEqual(1, this.transactionStatusList.Count);
            Assert.AreEqual(TransactionStatus.Committed, this.transactionStatusList[0]);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenSucceedFunction_FunctionResultIsReturned()
        {
            var actual = await this.target.InvokeAsync<object>(async () =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                return await Task<TransactionalOperationForObject>.Run(() => { return this.transactionalOperation; });
            });

            Assert.AreSame(this.transactionalOperation.Result, actual);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenFailedFunction_TransactionIsAborted()
        {
            this.transactionalOperation.CanCommit = false;
            await this.target.InvokeAsync<object>(async () =>
            {
                Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                return await Task<TransactionalOperationForObject>.Run(() => { return this.transactionalOperation; });
            });

            Assert.AreEqual(1, this.transactionStatusList.Count);
            Assert.AreEqual(TransactionStatus.Aborted, this.transactionStatusList[0]);
        }

        [TestMethod]
        public async Task InvokeAsyncFunction_GivenExceptionInFunction_TransactionIsAborted()
        {
            try
            {
                await this.target.InvokeAsync<int>(() =>
                {
                    Transaction.Current.TransactionCompleted += this.CurrentTransactionCompleted;
                    throw new Exception();
                });
            }
            catch
            { }

            Assert.AreEqual(this.maxRetry, this.transactionStatusList.Count);
            for (int i = 0; i < this.maxRetry; i++)
            {
                Assert.AreEqual(TransactionStatus.Aborted, this.transactionStatusList[i]);
            }
        }

        #endregion

        private void TargetError(object sender, InvokerErrorEventArgs e)
        {
            e.ExceptionHandled = this.handleException;
            this.errors.Add(e);
        }

        private void CurrentTransactionCompleted(object sender, TransactionEventArgs e)
        {
            this.transactionStatusList.Add(e.Transaction.TransactionInformation.Status);
        }

        private class TransactionalOperationForInt : ITransactionalOperation<int>
        {
            public bool CanCommit { get; set; }
            public int Result { get; set; }
        }

        private class TransactionalOperationForObject : ITransactionalOperation<object>
        {
            public bool CanCommit { get; set; }
            public object Result { get; set; }
        }
    }
}
